home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / python-debian / debian_bundle / debfile.py < prev    next >
Encoding:
Python Source  |  2008-10-30  |  10.0 KB  |  285 lines

  1. # DebFile: a Python representation of Debian .deb binary packages.
  2. # Copyright (C) 2007-2008   Stefano Zacchiroli  <zack@debian.org>
  3. # Copyright (C) 2007        Filippo Giunchedi   <filippo@debian.org>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  17.  
  18. import gzip
  19. import string
  20. import tarfile
  21. import zlib
  22.  
  23. from arfile import ArFile, ArError
  24. from changelog import Changelog
  25. from deb822 import Deb822
  26.  
  27. DATA_PART = 'data.tar'      # w/o extension
  28. CTRL_PART = 'control.tar'
  29. PART_EXTS = ['gz', 'bz2']   # possible extensions
  30. INFO_PART = 'debian-binary'
  31. MAINT_SCRIPTS = ['preinst', 'postinst', 'prerm', 'postrm', 'config']
  32.  
  33. CONTROL_FILE = 'control'
  34. CHANGELOG_NATIVE = 'usr/share/doc/%s/changelog.gz'  # with package stem
  35. CHANGELOG_DEBIAN = 'usr/share/doc/%s/changelog.Debian.gz'
  36. MD5_FILE = 'md5sums'
  37.  
  38.  
  39. class DebError(ArError):
  40.     pass
  41.  
  42.  
  43. class DebPart(object):
  44.     """'Part' of a .deb binary package.
  45.     
  46.     A .deb package is considered as made of 2 parts: a 'data' part
  47.     (corresponding to the 'data.tar.gz' archive embedded in a .deb) and a
  48.     'control' part (the 'control.tar.gz' archive). Each of them is represented
  49.     by an instance of this class. Each archive should be a compressed tar
  50.     archive; supported compression formats are: .tar.gz, .tar.bz2 .
  51.  
  52.     When referring to file members of the underlying .tar.gz archive, file
  53.     names can be specified in one of 3 formats "file", "./file", "/file". In
  54.     all cases the file is considered relative to the root of the archive. For
  55.     the control part the preferred mechanism is the first one (as in
  56.     deb.control.get_content('control') ); for the data part the preferred
  57.     mechanism is the third one (as in deb.data.get_file('/etc/vim/vimrc') ).
  58.     """
  59.  
  60.     def __init__(self, member):
  61.         self.__member = member  # arfile.ArMember file member
  62.         self.__tgz = None
  63.  
  64.     def tgz(self):
  65.         """Return a TarFile object corresponding to this part of a .deb
  66.         package.
  67.         
  68.         Despite the name, this method gives access to various kind of
  69.         compressed tar archives, not only gzipped ones.
  70.         """
  71.  
  72.         if self.__tgz is None:
  73.             name = self.__member.name
  74.             if name.endswith('.gz'):
  75.                 gz = gzip.GzipFile(fileobj=self.__member, mode='r')
  76.                 self.__tgz = tarfile.TarFile(fileobj=gz, mode='r')
  77.             elif name.endswith('.bz2'):
  78.                 # Tarfile's __init__ doesn't allow for r:bz2 modes, but the
  79.                 # open() classmethod does ...
  80.                 self.__tgz = tarfile.open(fileobj=self.__member, mode='r:bz2')
  81.             else:
  82.                 raise DebError("part '%s' has unexpected extension" % name)
  83.         return self.__tgz
  84.  
  85.     @staticmethod
  86.     def __normalize_member(fname):
  87.         """ try (not so hard) to obtain a member file name in a form relative
  88.         to the .tar.gz root and with no heading '.' """
  89.  
  90.         if fname.startswith('./'):
  91.             fname = fname[2:]
  92.         elif fname.startswith('/'):
  93.             fname = fname[1:]
  94.         return fname
  95.  
  96.     # XXX in some of the following methods, compatibility among >= 2.5 and <<
  97.     # 2.5 python versions had to be taken into account. TarFile << 2.5 indeed
  98.     # was buggied and returned member file names with an heading './' only for
  99.     # the *first* file member. TarFile >= 2.5 fixed this and has the heading
  100.     # './' for all file members.
  101.  
  102.     def has_file(self, fname):
  103.         """Check if this part contains a given file name."""
  104.  
  105.         fname = DebPart.__normalize_member(fname)
  106.         names = self.tgz().getnames()
  107.         return (('./' + fname in names) \
  108.                 or (fname in names)) # XXX python << 2.5 TarFile compatibility
  109.  
  110.     def get_file(self, fname):
  111.         """Return a file object corresponding to a given file name."""
  112.  
  113.         fname = DebPart.__normalize_member(fname)
  114.         try:
  115.             return (self.tgz().extractfile('./' + fname))
  116.         except KeyError:    # XXX python << 2.5 TarFile compatibility
  117.             return (self.tgz().extractfile(fname))
  118.  
  119.     def get_content(self, fname):
  120.         """Return the string content of a given file, or None (e.g. for
  121.         directories)."""
  122.  
  123.         f = self.get_file(fname)
  124.         content = None
  125.         if f:   # can be None for non regular or link files
  126.             content = f.read()
  127.             f.close()
  128.         return content
  129.  
  130.     # container emulation
  131.  
  132.     def __iter__(self):
  133.         return iter(self.tgz().getnames())
  134.  
  135.     def __contains__(self, fname):
  136.         return self.has_file(fname)
  137.  
  138.     def has_key(self, fname):
  139.         return self.has_file(fname)
  140.  
  141.     def __getitem__(self, fname):
  142.         return self.get_content(fname)
  143.  
  144.  
  145. class DebData(DebPart):
  146.  
  147.     pass
  148.  
  149.  
  150. class DebControl(DebPart):
  151.  
  152.     def scripts(self):
  153.         """ Return a dictionary of maintainer scripts (postinst, prerm, ...)
  154.         mapping script names to script text. """
  155.  
  156.         scripts = {}
  157.         for fname in MAINT_SCRIPTS:
  158.             if self.has_file(fname):
  159.                 scripts[fname] = self.get_content(fname)
  160.  
  161.         return scripts
  162.  
  163.     def debcontrol(self):
  164.         """ Return the debian/control as a Deb822 (a Debian-specific dict-like
  165.         class) object.
  166.         
  167.         For a string representation of debian/control try
  168.         .get_content('control') """
  169.  
  170.         return Deb822(self.get_content(CONTROL_FILE))
  171.  
  172.     def md5sums(self):
  173.         """ Return a dictionary mapping filenames (of the data part) to
  174.         md5sums. Fails if the control part does not contain a 'md5sum' file.
  175.  
  176.         Keys of the returned dictionary are the left-hand side values of lines
  177.         in the md5sums member of control.tar.gz, usually file names relative to
  178.         the file system root (without heading '/' or './'). """
  179.  
  180.         if not self.has_file(MD5_FILE):
  181.             raise DebError("'%s' file not found, can't list MD5 sums" %
  182.                     MD5_FILE)
  183.  
  184.         md5_file = self.get_file(MD5_FILE)
  185.         sums = {}
  186.         for line in md5_file.readlines():
  187.             # we need to support spaces in filenames, .split() is not enough
  188.             md5, fname = line.rstrip('\r\n').split(None, 1)
  189.             sums[fname] = md5
  190.         md5_file.close()
  191.         return sums
  192.  
  193.  
  194. class DebFile(ArFile):
  195.     """Representation of a .deb file (a Debian binary package)
  196.  
  197.     DebFile objects have the following (read-only) properties:
  198.         - version       debian .deb file format version (not related with the
  199.                         contained package version), 2.0 at the time of writing
  200.                         for all .deb packages in the Debian archive
  201.         - data          DebPart object corresponding to the data.tar.gz (or
  202.                         other compressed tar) archive contained in the .deb
  203.                         file
  204.         - control       DebPart object corresponding to the control.tar.gz (or
  205.                         other compressed tar) archive contained in the .deb
  206.                         file
  207.     """
  208.  
  209.     def __init__(self, filename=None, mode='r', fileobj=None):
  210.         ArFile.__init__(self, filename, mode, fileobj)
  211.         actual_names = set(self.getnames())
  212.  
  213.         def compressed_part_name(basename):
  214.             global PART_EXTS
  215.             candidates = [ '%s.%s' % (basename, ext) for ext in PART_EXTS ]
  216.             parts = actual_names.intersection(set(candidates))
  217.             if not parts:
  218.                 raise DebError("missing required part in given .deb" \
  219.                         " (expected one of: %s)" % candidates)
  220.             elif len(parts) > 1:
  221.                 raise DebError("too many parts in given .deb" \
  222.                         " (was looking for only one of: %s)" % candidates)
  223.             else:   # singleton list
  224.                 return list(parts)[0]
  225.  
  226.         if not INFO_PART in actual_names:
  227.             raise DebError("missing required part in given .deb" \
  228.                     " (expected: '%s')" % INFO_PART)
  229.  
  230.         self.__parts = {}
  231.         self.__parts[CTRL_PART] = DebControl(self.getmember(
  232.                 compressed_part_name(CTRL_PART)))
  233.         self.__parts[DATA_PART] = DebData(self.getmember(
  234.                 compressed_part_name(DATA_PART)))
  235.         self.__pkgname = None   # updated lazily by __updatePkgName
  236.  
  237.         f = self.getmember(INFO_PART)
  238.         self.__version = f.read().strip()
  239.         f.close()
  240.  
  241.     def __updatePkgName(self):
  242.         self.__pkgname = self.debcontrol()['package']
  243.  
  244.     version = property(lambda self: self.__version)
  245.     data = property(lambda self: self.__parts[DATA_PART])
  246.     control = property(lambda self: self.__parts[CTRL_PART])
  247.  
  248.     # proxy methods for the appropriate parts
  249.  
  250.     def debcontrol(self):
  251.         """ See .control.debcontrol() """
  252.         return self.control.debcontrol()
  253.  
  254.     def scripts(self):
  255.         """ See .control.scripts() """
  256.         return self.control.scripts()
  257.  
  258.     def md5sums(self):
  259.         """ See .control.md5sums() """
  260.         return self.control.md5sums()
  261.  
  262.     def changelog(self):
  263.         """ Return a Changelog object for the changelog.Debian.gz of the
  264.         present .deb package. Return None if no changelog can be found. """
  265.  
  266.         if self.__pkgname is None:
  267.             self.__updatePkgName()
  268.  
  269.         for fname in [ CHANGELOG_DEBIAN % self.__pkgname,
  270.                 CHANGELOG_NATIVE % self.__pkgname ]:
  271.             if self.data.has_file(fname):
  272.                 gz = gzip.GzipFile(fileobj=self.data.get_file(fname))
  273.                 raw_changelog = gz.read()
  274.                 gz.close()
  275.                 return Changelog(raw_changelog)
  276.         return None
  277.  
  278.  
  279. if __name__ == '__main__':
  280.     import sys
  281.     deb = DebFile(filename=sys.argv[1])
  282.     tgz = deb.control.tgz()
  283.     print tgz.getmember('control')
  284.  
  285.